package jp.dodododo.dao.impl;

import static jp.dodododo.dao.lock.OptimisticLocking.*;
import static jp.dodododo.dao.types.SQLTypes.*;

import java.io.InputStream;
import java.io.Reader;
import java.lang.annotation.Annotation;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.sql.DataSource;

import jp.dodododo.dao.Dao;
import jp.dodododo.dao.ExtendedExecuteUpdateDao;
import jp.dodododo.dao.IterationCallback;
import jp.dodododo.dao.annotation.Column;
import jp.dodododo.dao.annotation.Id;
import jp.dodododo.dao.annotation.IdDefSet;
import jp.dodododo.dao.annotation.Table;
import jp.dodododo.dao.annotation.Timestamp;
import jp.dodododo.dao.annotation.VersionNo;
import jp.dodododo.dao.columns.NoPersistentColumns;
import jp.dodododo.dao.columns.PersistentColumns;
import jp.dodododo.dao.context.CommandContext;
import jp.dodododo.dao.dialect.Default;
import jp.dodododo.dao.dialect.Dialect;
import jp.dodododo.dao.dialect.DialectManager;
import jp.dodododo.dao.empty_impl.DataSourceImpl;
import jp.dodododo.dao.exception.SQLRuntimeException;
import jp.dodododo.dao.handler.ResultSetHandler;
import jp.dodododo.dao.handler.impl.BeanResultSetHandler;
import jp.dodododo.dao.handler.impl.BigDecimalResultSetHandler;
import jp.dodododo.dao.handler.impl.MapResultSetHandler;
import jp.dodododo.dao.id.IdGenerator;
import jp.dodododo.dao.lock.Locking;
import jp.dodododo.dao.log.SqlLog;
import jp.dodododo.dao.log.SqlLogRegistry;
import jp.dodododo.dao.message.Message;
import jp.dodododo.dao.metadata.ColumnMetaData;
import jp.dodododo.dao.metadata.TableMetaData;
import jp.dodododo.dao.object.ObjectDesc;
import jp.dodododo.dao.object.ObjectDescFactory;
import jp.dodododo.dao.object.PropertyDesc;
import jp.dodododo.dao.paging.LimitOffset;
import jp.dodododo.dao.paging.PagingResultSet;
import jp.dodododo.dao.sql.node.Node;
import jp.dodododo.dao.sql.parse.SqlParser;
import jp.dodododo.dao.types.SQLType;
import jp.dodododo.dao.util.CacheUtil;
import jp.dodododo.dao.util.CaseInsensitiveMap;
import jp.dodododo.dao.util.ClassUtil;
import jp.dodododo.dao.util.ConnectionUtil;
import jp.dodododo.dao.util.DataSourceUtil;
import jp.dodododo.dao.util.EnumUtil;
import jp.dodododo.dao.util.InputStreamReaderUtil;
import jp.dodododo.dao.util.InputStreamUtil;
import jp.dodododo.dao.util.OgnlUtil;
import jp.dodododo.dao.util.PreparedStatementUtil;
import jp.dodododo.dao.util.ReaderUtil;
import jp.dodododo.dao.util.ResultSetUtil;
import jp.dodododo.dao.util.StatementUtil;
import jp.dodododo.dao.util.StringUtil;
import jp.dodododo.dao.util.TypesUtil;
import jp.dodododo.dao.value.ParameterValue;
import jp.dodododo.dao.wrapper.ConnectionWrapper;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class DaoImpl implements Dao, ExtendedExecuteUpdateDao {

	private DataSource dataSource;

	private SqlLogRegistry sqlLogRegistry;

	private static String defaultEncoding = "JISAutoDetect";

	private String encoding;

	private static final String DEFAULT_WHERE_COLUMN_PREFIX = "where___";

	private String whereColumnPrefix;

	{
		sqlLogRegistry = new SqlLogRegistry();
		encoding = defaultEncoding;
		whereColumnPrefix = DEFAULT_WHERE_COLUMN_PREFIX;
	}

	/**
	 * @see #setDataSource(DataSource)
	 */
	public DaoImpl() {
	}

	public DaoImpl(Connection connection) {
		init(connection);
	}

	private void init(final Connection connection) {
		final Connection connectionWrapper = new ConnectionWrapper(connection) {
			@Override
			public void close() {
				// empty
			}
		};
		this.dataSource = new DataSourceImpl() {
			@Override
			public Connection getConnection() throws SQLException {
				return connectionWrapper;
			}
		};
	}

	public DaoImpl(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public <ENTITY> int insert(ENTITY entity) {
		return insert(entity, null, null, OPTIMISTIC_LOCKING);
	}

	private Map<String, TableMetaData> tableMetaDataCache = CacheUtil.cacheMap(new CaseInsensitiveMap<TableMetaData>());

	private TableMetaData getTableMetaData(String tableName) {
		TableMetaData metaData = tableMetaDataCache.get(tableName);
		if (metaData != null) {
			return metaData;
		}
		Connection connection = null;
		try {
			connection = getConnection();
			metaData = new TableMetaData(connection, tableName);
			tableMetaDataCache.put(tableName, metaData);
			return metaData;
		} finally {
			ConnectionUtil.close(connection);
		}
	}

	private Connection getConnection() {
		return DataSourceUtil.getConnection(dataSource);
	}

	private <ENTITY> String getTableName(ENTITY entity) {
		Class<?> entityClass = entity.getClass();
		Table table = entityClass.getAnnotation(Table.class);
		if (table != null) {
			return table.value();
		} else {
			return getTableName(entityClass);
		}
	}

	private String getTableName(Class<?> entityClass) {
		String className = ClassUtil.getShortName(entityClass);
		return StringUtil.decamelize(className);
	}

	public <ENTITY> int delete(ENTITY entity) {
		return delete(entity, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int update(ENTITY entity) {
		return update(entity, null, null, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int[] delete(Collection<ENTITY> entities) {
		return delete(entities, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int[] insert(Collection<ENTITY> entities) {
		return insert(entities, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int[] update(Collection<ENTITY> entities) {
		return update(entities, OPTIMISTIC_LOCKING);
	}

	private <ROW> List<ROW> select(String sql, Map<String, Object> arg, IterationCallback<ROW> callback, ResultSetHandler<?> handler) {
		Connection connection = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try {
			connection = getConnection();
			Dialect dialect = DialectManager.getDialect(connection);
			if (sql.contains(" |\n|\r|\t") == false) {
				sql = readSqlFile(sql, dialect);
			}
			LimitOffset limitOffset = null;
			if (arg != null && arg.containsKey(LimitOffset.KEYWORD) == true) {
				limitOffset = (LimitOffset) arg.get(LimitOffset.KEYWORD);
				try {
					sql = dialect.limitOffsetSql(sql, limitOffset);
					limitOffset = null;
				} catch (UnsupportedOperationException ignore) {
				}
			}
			Map<String, ParameterValue> values = createParameterValues(arg);
			ps = createPreparedStatement(connection, sql, values);
			rs = PreparedStatementUtil.executeQuery(ps);
			if (limitOffset != null) {
				rs = new PagingResultSet(rs, limitOffset);
			}
			rs = dialect.resultSet(rs);
			handler.handle(rs);
			return callback.getResult();
		} catch (SQLException e) {
			throw new SQLRuntimeException(e);
		} finally {
			try {
				ResultSetUtil.close(rs);
			} finally {
				try {
					StatementUtil.close(ps);
				} finally {
					ConnectionUtil.close(connection);
				}
			}
		}
	}

	private String readSqlFile(final String sql, Dialect dialect) {
		try {
			ClassLoader loader = Thread.currentThread().getContextClassLoader();
			InputStream is = loader.getResourceAsStream(getSqlFilePath(sql, dialect, loader));
			if (is != null) {
				Reader reader = InputStreamReaderUtil.create(is, encoding);
				return ReaderUtil.readText(reader);
			}
			return sql;
		} catch (RuntimeException e) {
			// function呼ぶだけかもしれないから実行時の例外にする
			return sql;
		}
	}

	protected String getSqlFilePath(String sqlPath, Dialect dialect, ClassLoader loader) {
		String base = getBase(sqlPath);
		String dialectPath = base + dialect.getSuffix() + ".sql";
		String standardPath = base + ".sql";
		if (loader.getResource(dialectPath) != null) {
			return dialectPath;
		}
		if (loader.getResource(standardPath) != null) {
			return standardPath;
		}
		return sqlPath;
	}

	private static String getBase(String sqlPath) {
		if (sqlPath.endsWith(".sql")) {
			return sqlPath.substring(0, sqlPath.length() - ".sql".length());
		}
		return sqlPath;
	}

	protected <ROW> ResultSetHandler<?> createResultSetHandler(Class<ROW> entityClass, IterationCallback<ROW> callback) {
		return new BeanResultSetHandler<ROW>(callback, entityClass);
	}

	public <ROW> ROW selectOne(String sql, Map<String, Object> arg, Class<ROW> entityClass) throws SQLRuntimeException {
		List<ROW> rows = select(sql, arg, entityClass);
		if (rows.size() == 0) {
			return null;
		} else if (rows.size() == 1) {
			return rows.get(0);
		} else {
			throw new IllegalStateException(Message.getMessage("00003"));
		}
	}

	public <ROW> ROW selectOne(String sql, Class<ROW> entityClass) {
		return selectOne(sql, null, entityClass);
	}

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public <ENTITY> int insert(ENTITY entity, NoPersistentColumns npc) {
		return insert(entity, null, npc.getColumnList(), OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int insert(ENTITY entity, PersistentColumns pc) {
		return insert(entity, pc.getColumnList(), null, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int[] insert(Collection<ENTITY> entities, NoPersistentColumns npc) {
		return insert(entities, npc, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int[] insert(Collection<ENTITY> entities, PersistentColumns pc) {
		return insert(entities, pc, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int update(ENTITY entity, NoPersistentColumns npc) {
		return update(entity, npc, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int update(ENTITY entity, PersistentColumns pc) {
		return update(entity, pc, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int[] update(Collection<ENTITY> entities, NoPersistentColumns npc) {
		return update(entities, npc, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int[] update(Collection<ENTITY> entities, PersistentColumns pc) {
		return update(entities, pc, OPTIMISTIC_LOCKING);
	}

	public <ENTITY> int insert(ENTITY entity, Locking locking) {
		return insert(entity, null, null, locking);
	}

	public <ENTITY> int insert(ENTITY entity, NoPersistentColumns npc, Locking locking) {
		return insert(entity, null, npc.getColumnList(), locking);
	}

	public <ENTITY> int insert(ENTITY entity, PersistentColumns pc, Locking locking) {
		return insert(entity, pc.getColumnList(), null, locking);
	}

	private List<String> getUpdateColumnNames(TableMetaData tableMetaData, List<String> persistentColumns, List<String> moPersistentColumns) {
		if (persistentColumns != null && persistentColumns.isEmpty() == false) {
			return persistentColumns;
		}
		List<String> columnNames = new ArrayList<String>();
		List<String> allColumnNames = tableMetaData.getColumnNames();
		for (String columnName : allColumnNames) {
			if (moPersistentColumns == null || moPersistentColumns.contains(columnName) == false) {
				columnNames.add(columnName);
			}
		}
		return columnNames;
	}

	public <ENTITY> int[] insert(Collection<ENTITY> entities, Locking locking) {
		return insertBatch(entities, null, null, locking);
	}

	public <ENTITY> int[] insert(Collection<ENTITY> entities, NoPersistentColumns npc, Locking locking) {
		return insertBatch(entities, null, npc.getColumnList(), locking);
	}

	public <ENTITY> int[] insert(Collection<ENTITY> entities, PersistentColumns pc, Locking locking) {
		return insertBatch(entities, pc.getColumnList(), null, locking);
	}

	public <ENTITY> int update(ENTITY entity, Locking locking) {
		return update(entity, null, null, locking);
	}

	public <ENTITY> int update(ENTITY entity, NoPersistentColumns npc, Locking locking) {
		return update(entity, null, npc.getColumnList(), locking);
	}

	public <ENTITY> int update(ENTITY entity, PersistentColumns pc, Locking locking) {
		return update(entity, pc.getColumnList(), null, locking);
	}

	public <ENTITY> int[] update(Collection<ENTITY> entities, Locking locking) {
		return updateBatch(entities, null, null, locking);
	}

	public <ENTITY> int[] update(Collection<ENTITY> entities, NoPersistentColumns npc, Locking locking) {
		return updateBatch(entities, null, npc.getColumnList(), locking);
	}

	public <ENTITY> int[] update(Collection<ENTITY> entities, PersistentColumns pc, Locking locking) {
		return updateBatch(entities, pc.getColumnList(), null, locking);
	}

	private <ENTITY> int[] insertBatch(Collection<ENTITY> entities, List<String> pc, List<String> npc, Locking locking) {
		if (entities.isEmpty() == true) {
			return new int[] { 0 };
		}
		Iterator<ENTITY> iterator = entities.iterator();
		if (iterator.hasNext() == false) {
			return new int[] { 0 };
		}
		Connection connection = null;
		PreparedStatement ps = null;
		try {
			connection = getConnection();
			ENTITY entity = iterator.next();
			prepareInsert(entity, locking, connection);
			String tableName = getTableName(entity);
			TableMetaData tableMetaData = getTableMetaData(tableName);
			List<String> updateColumnNames = getUpdateColumnNames(tableMetaData, pc, npc);

			Map<String, ParameterValue> values = getUpdateParameterValues(entity, updateColumnNames, tableMetaData);
			String sql = createInsertSql(tableName, updateColumnNames, values);
			ps = createPreparedStatement(connection, sql, values);
			PreparedStatementUtil.addBatch(ps);

			for (; iterator.hasNext();) {
				ENTITY e = iterator.next();
				prepareInsert(e, locking, connection);

				Node node = parse(sql);
				values = getUpdateParameterValues(e, updateColumnNames, tableMetaData);
				Dialect dialect = DialectManager.getDialect(connection);
				CommandContext ctx = new CommandContext(dialect);
				ctx.addArgs(values);
				node.accept(ctx);
				List<Object> bindVariables = ctx.getBindVariables();
				List<Integer> bindVariableTypes = ctx.getBindVariableTypes();
				bindArgs(ps, bindVariables, bindVariableTypes, dialect);
				PreparedStatementUtil.addBatch(ps);
			}
			return PreparedStatementUtil.executeBatch(ps);
		} finally {
			try {
				StatementUtil.close(ps);
			} finally {
				ConnectionUtil.close(connection);
			}
		}
	}

	protected <ENTITY> void prepareInsert(ENTITY entity, Locking locking, Connection connection) {
		setIds(entity, connection);
		if (locking == OPTIMISTIC_LOCKING) {
			setVersionNos(entity, true);
			setTimestamps(entity);
		}
	}

	private <ENTITY> void setTimestamps(ENTITY entity) {
		ObjectDesc objectDesc = ObjectDescFactory.getObjectDesc(entity);
		int size = objectDesc.getPropertyDescSize();
		for (int i = 0; i < size; i++) {
			PropertyDesc propertyDesc = objectDesc.getPropertyDesc(i);
			Timestamp timestamp = propertyDesc.getAnnotation(Timestamp.class);
			if (timestamp == null) {
				continue;
			}
			String expression = timestamp.value();
			Object value = OgnlUtil.getValue(expression, new HashMap<String, Object>());
			propertyDesc.setValue(entity, value);
		}
	}

	private <ENTITY> void setVersionNos(ENTITY entity, boolean isInsert) {
		ObjectDesc objectDesc = ObjectDescFactory.getObjectDesc(entity);
		int size = objectDesc.getPropertyDescSize();
		for (int i = 0; i < size; i++) {
			PropertyDesc propertyDesc = objectDesc.getPropertyDesc(i);
			VersionNo versionNo = propertyDesc.getAnnotation(VersionNo.class);
			if (versionNo == null) {
				continue;
			}
			if (isInsert == true) {
				propertyDesc.setValue(entity, versionNo.value());
			} else {
				long oldVersion = Long.parseLong(propertyDesc.getValue(entity).toString());
				long newVersion = oldVersion + 1;
				propertyDesc.setValue(entity, newVersion);
			}
		}
	}

	private <ENTITY> void setIds(ENTITY entity, Connection connection) {
		ObjectDesc objectDesc = ObjectDescFactory.getObjectDesc(entity);
		int size = objectDesc.getPropertyDescSize();
		Dialect dialect = DialectManager.getDialect(connection);
		for (int i = 0; i < size; i++) {
			PropertyDesc propertyDesc = objectDesc.getPropertyDesc(i);
			setId(entity, propertyDesc, connection, dialect);
		}
	}

	private <ENTITY> void setId(ENTITY entity, PropertyDesc propertyDesc, Connection connection, Dialect dialect) {
		Map<Class<? extends Dialect>, IdDefSet> idDefs = getIdDefs(propertyDesc);
		IdDefSet idDefSet = idDefs.get(dialect.getClass());
		if (idDefSet == null) {
			idDefSet = idDefs.get(Default.class);
		}
		if (idDefSet != null) {
			Class<? extends IdGenerator> type = idDefSet.type();
			IdGenerator generator;
			if (Enum.class.isAssignableFrom(type)) {
				@SuppressWarnings("unchecked")
				Class<? extends Enum> enumClass = (Class<? extends Enum>) type;
				generator = EnumUtil.firstValue(enumClass, IdGenerator.class);
			} else {
				generator = ClassUtil.newInstance(type);
			}
			Object idValue = generator.generate(connection, idDefSet.name());
			propertyDesc.setValue(entity, idValue);
		}
	}

	private Map<Class<? extends Dialect>, IdDefSet> getIdDefs(PropertyDesc propertyDesc) {
		Map<Class<? extends Dialect>, IdDefSet> ret = new HashMap<Class<? extends Dialect>, IdDefSet>();
		Id idAnnotation = propertyDesc.getAnnotation(Id.class);
		if (idAnnotation == null) {
			return ret;
		}
		IdDefSet[] idDefSets = idAnnotation.value();
		for (IdDefSet idDefSet : idDefSets) {
			Class<? extends Dialect> dialectDef = idDefSet.db();
			ret.put(dialectDef, idDefSet);
		}
		return ret;
	}

	protected <ENTITY> void prepareUpdate(ENTITY entity, Locking locking) {
		if (locking == OPTIMISTIC_LOCKING) {
			setVersionNos(entity, false);
			setTimestamps(entity);
		}
	}

	private Node parse(String sql) {
		Node node = nodeCash.get(sql);
		if (node != null) {
			return node;
		}
		SqlParser sqlParser = new SqlParser(sql);
		node = sqlParser.parse();
		nodeCash.put(sql, node);
		return node;
	}

	private Map<String, Node> nodeCash = CacheUtil.cacheMap();

	private <ENTITY> int insert(ENTITY entity, List<String> pc, List<String> npc, Locking locking) {
		if (entity == null) {
			throw new NullPointerException(Message.getMessage("00001"));
		}
		String tableName = getTableName(entity);
		TableMetaData tableMetaData = getTableMetaData(tableName);
		List<String> updateColumnNames = getUpdateColumnNames(tableMetaData, pc, npc);
		Connection connection = null;
		try {
			connection = getConnection();
			prepareInsert(entity, locking, connection);
		} finally {
			ConnectionUtil.close(connection);
		}
		Map<String, ParameterValue> values = getUpdateParameterValues(entity, updateColumnNames, tableMetaData);

		String sql = createInsertSql(tableName, updateColumnNames, values);
		return executeUpdate(values, sql);
	}

	private PreparedStatement createPreparedStatement(Connection connection, String sql, Map<String, ParameterValue> values) {
		Node node = parse(sql);
		Dialect dialect = DialectManager.getDialect(connection);
		CommandContext ctx = new CommandContext(dialect);
		ctx.addArgs(values);
		node.accept(ctx);
		List<Object> bindVariables = ctx.getBindVariables();
		List<Integer> bindVariableTypes = ctx.getBindVariableTypes();
		String executableSql = ctx.getSql();
		PreparedStatement ps = ConnectionUtil.prepareStatement(connection, executableSql);
		bindArgs(ps, bindVariables, bindVariableTypes, executableSql, dialect);
		return ps;
	}

	private void bindArgs(PreparedStatement ps, List<Object> bindVariables, List<Integer> bindVariableTypes, Dialect dialect) {
		String sql = this.sqlLogRegistry.getLast().getRawSql();
		bindArgs(ps, bindVariables, bindVariableTypes, sql, dialect);
	}

	private void bindArgs(PreparedStatement ps, List<Object> bindVariables, List<Integer> bindVariableTypes, String sql, Dialect dialect) {
		logSql(sql, bindVariables, bindVariableTypes, dialect);
		if (bindVariables == null || bindVariables.isEmpty()) {
			return;
		}
		for (int i = 0; i < bindVariables.size(); ++i) {
			try {
				Object value = bindVariables.get(i);
				if (value == null) {
					ps.setNull(i + 1, bindVariableTypes.get(i));
				} else if (value instanceof InputStream) {
					InputStream is = (InputStream) value;
					ps.setBinaryStream(i + 1, is, InputStreamUtil.available(is));
				} else {
					ps.setObject(i + 1, value, bindVariableTypes.get(i));
				}
			} catch (SQLException e) {
				throw new SQLRuntimeException(e);
			}
		}
	}

	private String createInsertSql(String tableName, List<String> updateColumnNames, Map<String, ParameterValue> values) {
		StringBuilder sql = new StringBuilder(1024);
		sql.append("INSERT INTO ").append(tableName).append(" ( ");
		for (String updateColumnName : updateColumnNames) {
			sql.append(updateColumnName).append(", ");
		}
		sql.setLength(sql.length() - 2);
		sql.append(" ) VALUES ( ");
		for (String updateColumnName : updateColumnNames) {
			sql.append("/*").append(updateColumnName).append("*/").append(getDummyValString(updateColumnName, values)).append(" , ");
		}
		sql.setLength(sql.length() - 2);
		sql.append(")");
		return sql.toString();
	}

	private String getDummyValString(String parameterName, Map<String, ParameterValue> values) {
		// todo 0ではなく、ダミーのデータを表示させたい
		// けど、速度的な事もあるから、とりあえず0
		// ParameterValue value = values.get(parameterName);
		// int dataType = value.getDataType();
		return "0";
	}

	private <ENTITY> Map<String, ParameterValue> getUpdateParameterValues(ENTITY entity, List<String> updateColumnNames,
			TableMetaData tableMetaData) {
		Map<String, ParameterValue> parameterValues = new HashMap<String, ParameterValue>(updateColumnNames.size());
		for (String columnName : updateColumnNames) {
			Object value = getValue(entity, columnName);
			int dataType = tableMetaData.getColumnMetaData(columnName).getDataType();
			ParameterValue pv = new ParameterValue(columnName, dataType, value);
			parameterValues.put(columnName, pv);
		}
		return parameterValues;
	}

	private Object getValue(Object entity, String columnName) {
		ObjectDesc objectDesc = ObjectDescFactory.getObjectDesc(entity.getClass());
		PropertyDesc propertyDesc = objectDesc.getPropertyDesc(columnName);
		return propertyDesc.getValue(entity);
	}

	private <ENTITY> int[] updateBatch(Collection<ENTITY> entities, List<String> pc, List<String> npc, Locking locking) {
		if (entities.isEmpty() == true) {
			return new int[] { 0 };
		}
		Iterator<ENTITY> iterator = entities.iterator();
		if (iterator.hasNext() == false) {
			return new int[] { 0 };
		}
		Connection connection = null;
		PreparedStatement ps = null;
		try {
			connection = getConnection();
			ENTITY entity = iterator.next();
			String tableName = getTableName(entity);
			TableMetaData tableMetaData = getTableMetaData(tableName);
			List<String> updateColumnNames = getUpdateColumnNames(tableMetaData, pc, npc);

			Map<String, ParameterValue> values = new HashMap<String, ParameterValue>();
			List<String> whereColumnNames = getWhereColumnNames(tableMetaData, entity, locking);
			addWhereValues(entity, whereColumnNames, tableMetaData, values);
			prepareUpdate(entity, locking);
			values.putAll(getUpdateParameterValues(entity, updateColumnNames, tableMetaData));
			String sql = createUpdateSql(tableName, updateColumnNames, values, whereColumnNames);
			ps = createPreparedStatement(connection, sql, values);
			PreparedStatementUtil.addBatch(ps);

			for (; iterator.hasNext();) {
				ENTITY e = iterator.next();

				Node node = parse(sql);
				values = new HashMap<String, ParameterValue>();
				addWhereValues(e, whereColumnNames, tableMetaData, values);
				prepareUpdate(e, locking);
				values.putAll(getUpdateParameterValues(e, updateColumnNames, tableMetaData));
				Dialect dialect = DialectManager.getDialect(connection);
				CommandContext ctx = new CommandContext(dialect);
				ctx.addArgs(values);
				node.accept(ctx);
				List<Object> bindVariables = ctx.getBindVariables();
				List<Integer> bindVariableTypes = ctx.getBindVariableTypes();
				bindArgs(ps, bindVariables, bindVariableTypes, dialect);
				PreparedStatementUtil.addBatch(ps);
			}
			return PreparedStatementUtil.executeBatch(ps);
		} finally {
			try {
				StatementUtil.close(ps);
			} finally {
				ConnectionUtil.close(connection);
			}
		}
	}

	private <ENTITY> int update(ENTITY entity, List<String> pc, List<String> npc, Locking locking) {
		if (entity == null) {
			throw new NullPointerException(Message.getMessage("00001"));
		}
		String tableName = getTableName(entity);
		TableMetaData tableMetaData = getTableMetaData(tableName);
		List<String> updateColumnNames = getUpdateColumnNames(tableMetaData, pc, npc);
		Map<String, ParameterValue> values = new HashMap<String, ParameterValue>();
		List<String> whereColumnNames = getWhereColumnNames(tableMetaData, entity, locking);
		addWhereValues(entity, whereColumnNames, tableMetaData, values);
		prepareUpdate(entity, locking);
		values.putAll(getUpdateParameterValues(entity, updateColumnNames, tableMetaData));
		String sql = createUpdateSql(tableName, updateColumnNames, values, whereColumnNames);
		return executeUpdate(values, sql);
	}

	private <ENTITY> void addWhereValues(ENTITY entity, List<String> whereColumnNames, TableMetaData tableMetaData,
			Map<String, ParameterValue> values) {
		for (String columnName : whereColumnNames) {
			Object value = null;
			value = getValue(entity, columnName);
			String name = whereColumnPrefix + columnName;
			int dataType = tableMetaData.getColumnMetaData(columnName).getDataType();
			ParameterValue pv = new ParameterValue(name, dataType, value);
			values.put(name, pv);
		}
	}

	private <ENTITY> List<String> getWhereColumnNames(TableMetaData tableMetaData, ENTITY entity, Locking locking) {
		List<String> whereColumnNames = new ArrayList<String>();
		List<String> pkColumnNames = tableMetaData.getPkColumnNames();
		if (pkColumnNames.isEmpty() == true) {
			throw new IllegalStateException(Message.getMessage("00002"));
		}
		whereColumnNames.addAll(pkColumnNames);

		if (locking == OPTIMISTIC_LOCKING) {
			addVersionNoColumnNames(entity, tableMetaData, whereColumnNames);
			addTimestampColumnNames(entity, tableMetaData, whereColumnNames);
		}

		return whereColumnNames;
	}

	private <ENTITY> void addVersionNoColumnNames(ENTITY entity, TableMetaData tableMetaData, List<String> whereColumnNames) {
		addWhereColumnNames(entity, tableMetaData, VersionNo.class, whereColumnNames);
	}

	private <ENTITY> void addTimestampColumnNames(ENTITY entity, TableMetaData tableMetaData, List<String> whereColumnNames) {
		addWhereColumnNames(entity, tableMetaData, Timestamp.class, whereColumnNames);
	}

	private <ENTITY> void addWhereColumnNames(ENTITY entity, TableMetaData tableMetaData, Class<? extends Annotation> annotationClass,
			List<String> whereColumnNames) {
		ObjectDesc objectDesc = ObjectDescFactory.getObjectDesc(entity);
		int size = objectDesc.getPropertyDescSize();
		for (int i = 0; i < size; i++) {
			PropertyDesc propertyDesc = objectDesc.getPropertyDesc(i);
			Annotation annotation = propertyDesc.getAnnotation(annotationClass);
			if (annotation != null) {
				String propertyName = getColumnName(propertyDesc);
				ColumnMetaData columnMetaData = tableMetaData.getColumnMetaData(propertyName);
				// whereColumnNames.add(whereColumnPrefix +
				// columnMetaData.getColumnName());
				whereColumnNames.add(columnMetaData.getColumnName());
			}
		}
	}

	private String getColumnName(PropertyDesc propertyDesc) {
		Column column = propertyDesc.getAnnotation(Column.class);
		if (column != null) {
			return column.value();
		}
		return propertyDesc.getPropertyName();
	}

	private String createUpdateSql(String tableName, List<String> updateColumnNames, Map<String, ParameterValue> values,
			List<String> whereColumnNames) {
		StringBuilder sql = new StringBuilder(1024);
		sql.append("UPDATE ");
		sql.append(tableName);
		sql.append(" SET ");
		for (String updateColumnName : updateColumnNames) {
			sql.append(updateColumnName);
			sql.append(" = ");
			sql.append("/*" + updateColumnName + "*/" + getDummyValString(updateColumnName, values) + " ");
			sql.append(", ");
		}
		sql.setLength(sql.length() - 2);
		sql.append("WHERE ");
		for (String columnName : whereColumnNames) {
			sql.append(columnName);
			sql.append(" = ");
			sql.append("/*").append(whereColumnPrefix).append(columnName).append("*/");
			sql.append(getDummyValString(columnName, values));
			sql.append(" ");
			sql.append("AND ");
		}
		sql.setLength(sql.length() - 4);
		return sql.toString();
	}

	public <ENTITY> int delete(ENTITY entity, Locking locking) {
		if (entity == null) {
			throw new NullPointerException(Message.getMessage("00001"));
		}
		String tableName = getTableName(entity);
		TableMetaData tableMetaData = getTableMetaData(tableName);
		Map<String, ParameterValue> values = new HashMap<String, ParameterValue>();
		List<String> whereColumnNames = getWhereColumnNames(tableMetaData, entity, locking);

		addWhereValues(entity, whereColumnNames, tableMetaData, values);
		String sql = createDeleteSql(tableName, whereColumnNames, values);
		return executeUpdate(values, sql);
	}

	private String createDeleteSql(String tableName, List<String> whereColumnNames, Map<String, ParameterValue> values) {
		StringBuilder sql = new StringBuilder(1024);
		sql.append("DELETE FROM ").append(tableName).append(" WHERE ");
		for (String columnName : whereColumnNames) {
			sql.append(columnName).append(" = ");
			sql.append("/*").append(whereColumnPrefix).append(columnName).append("*/");
			sql.append(getDummyValString(columnName, values)).append(" ");
			sql.append(", ");
		}
		sql.setLength(sql.length() - 2);
		return sql.toString();
	}

	public <ENTITY> int[] delete(Collection<ENTITY> entities, Locking locking) {
		return deleteBatch(entities, locking);
	}

	private <ENTITY> int[] deleteBatch(Collection<ENTITY> entities, Locking locking) {
		if (entities.isEmpty() == true) {
			return new int[] { 0 };
		}
		Iterator<ENTITY> iterator = entities.iterator();
		if (iterator.hasNext() == false) {
			return new int[] { 0 };
		}
		Connection connection = null;
		PreparedStatement ps = null;
		try {
			connection = getConnection();
			ENTITY entity = iterator.next();
			prepareDelete(entity, locking);
			String tableName = getTableName(entity);
			TableMetaData tableMetaData = getTableMetaData(tableName);

			Map<String, ParameterValue> values = new HashMap<String, ParameterValue>();
			List<String> whereColumnNames = getWhereColumnNames(tableMetaData, entity, locking);
			addWhereValues(entity, whereColumnNames, tableMetaData, values);
			String sql = createDeleteSql(tableName, whereColumnNames, values);
			ps = createPreparedStatement(connection, sql, values);
			PreparedStatementUtil.addBatch(ps);

			for (; iterator.hasNext();) {
				ENTITY e = iterator.next();
				prepareDelete(e, locking);

				Node node = parse(sql);
				values = new HashMap<String, ParameterValue>();
				addWhereValues(e, whereColumnNames, tableMetaData, values);
				Dialect dialect = DialectManager.getDialect(connection);
				CommandContext ctx = new CommandContext(dialect);
				ctx.addArgs(values);
				node.accept(ctx);
				List<Object> bindVariables = ctx.getBindVariables();
				List<Integer> bindVariableTypes = ctx.getBindVariableTypes();
				bindArgs(ps, bindVariables, bindVariableTypes, dialect);
				PreparedStatementUtil.addBatch(ps);
			}
			return PreparedStatementUtil.executeBatch(ps);
		} finally {
			try {
				StatementUtil.close(ps);
			} finally {
				ConnectionUtil.close(connection);
			}
		}
	}

	private <ENTITY> void prepareDelete(ENTITY entity, Locking locking) {

	}

	public int executeUpdate(String sql, Map<String, Object> arg) {
		Map<String, ParameterValue> values = createParameterValues(arg);
		return executeUpdate(values, sql);
	}

	private int executeUpdate(Map<String, ParameterValue> values, String sql) {
		Connection connection = null;
		PreparedStatement ps = null;
		try {
			connection = getConnection();
			ps = createPreparedStatement(connection, sql, values);
			return PreparedStatementUtil.executeUpdate(ps);
		} finally {
			try {
				StatementUtil.close(ps);
			} finally {
				ConnectionUtil.close(connection);
			}
		}
	}

	private Map<String, ParameterValue> createParameterValues(Map<String, Object> arg) {
		Map<String, ParameterValue> parameterValues = new HashMap<String, ParameterValue>();
		if (arg == null) {
			return parameterValues;
		}
		Set<Entry<String, Object>> entrySet = arg.entrySet();
		for (Iterator<Entry<String, Object>> iter = entrySet.iterator(); iter.hasNext();) {
			Entry<String, Object> entry = iter.next();
			String name = entry.getKey();
			Object value = entry.getValue();
			int dataType = TypesUtil.getSQLType(value).getType();
			ParameterValue pv = new ParameterValue(name, dataType, value);
			parameterValues.put(name, pv);
		}
		return parameterValues;
	}

	private static final Log logger = LogFactory.getLog(DaoImpl.class);

	private void logSql(String sql, List<Object> args, List<Integer> types, Dialect dialect) {
		String completeSql = getCompleteSql(sql, args, types, dialect);

		SqlLog sqlLog = new SqlLog(sql, completeSql, args, types);
		this.sqlLogRegistry.add(sqlLog);

		if (logger.isDebugEnabled() == true) {
			logger.debug("\n" + completeSql);
		}
	}

	protected String getCompleteSql(String sql, List<Object> args, List<Integer> types, Dialect dialect) {
		if (args == null || args.isEmpty() == true) {
			return StringUtil.trimLine(sql);
		}
		StringBuilder buf = new StringBuilder(512);
		int pos = 0;
		int pos2 = 0;
		int index = 0;
		while (true) {
			pos = sql.indexOf('?', pos2);
			if (pos > 0) {
				buf.append(sql.substring(pos2, pos));
				buf.append(getBindVariableText(args.get(index), types.get(index), dialect));
				index++;
				pos2 = pos + 1;
			} else {
				buf.append(sql.substring(pos2));
				break;
			}
		}
		String completeSql = buf.toString();
		return StringUtil.trimLine(completeSql);
	}

	protected String getBindVariableText(Object value, Integer type, Dialect dialect) {
		if (value == null) {
			return "NULL";
		}
		SQLType sqlType = type != null ? TypesUtil.getSQLType(type.intValue()) : TypesUtil.getSQLType(value);

		if (sqlType == STRING) {
			return "'" + value + "'";
		} else if (sqlType == NUMBER) {
			return value.toString();
		} else if (sqlType == DATE) {
			return dialect.toDateString((java.util.Date) value);
		} else if (sqlType == TIMESTAMP) {
			return dialect.toTimestampString((java.util.Date) value);
		} else if (sqlType == BOOLEAN) {
			return value.toString();
		} else {
			return "'" + value.toString() + "'";
		}
	}

	public void setSqlLogRegistry(SqlLogRegistry sqlLogRegistry) {
		this.sqlLogRegistry = sqlLogRegistry;
	}

	public void setEncoding(String encoding) {
		this.encoding = encoding;
	}

	public Map<String, Object> selectOneMap(String sql, Map<String, Object> arg) {
		List<Map<String, Object>> rows = selectMap(sql, arg);
		if (rows.size() == 0) {
			return null;
		} else if (rows.size() == 1) {
			return rows.get(0);
		} else {
			throw new IllegalStateException(Message.getMessage("00003"));
		}
	}

	public Map<String, Object> selectOneMap(String sql) {
		return selectOneMap(sql, null);
	}

	public BigDecimal selectOneNumber(String sql, Map<String, Object> arg) throws SQLRuntimeException {
		IterationCallback<BigDecimal> callback = new ListIterationCallback<BigDecimal>();
		ResultSetHandler<?> handler = new BigDecimalResultSetHandler(callback);

		List<BigDecimal> rows = select(sql, arg, callback, handler);
		if (rows.size() == 0) {
			return null;
		} else if (rows.size() == 1) {
			return rows.get(0);
		} else {
			throw new IllegalStateException(Message.getMessage("00003"));
		}
	}

	public BigDecimal selectOneNumber(String sql) throws SQLRuntimeException {
		return selectOneNumber(sql, null);
	}

	public <ROW> List<ROW> select(String sql, Map<String, Object> arg, Class<ROW> entityClass) throws SQLRuntimeException {
		return select(sql, arg, entityClass, new ListIterationCallback<ROW>());
	}

	public <ROW> List<ROW> select(String sql, Map<String, Object> arg, Class<ROW> entityClass, IterationCallback<ROW> callback)
			throws SQLRuntimeException {
		ResultSetHandler<?> handler = createResultSetHandler(entityClass, callback);
		return select(sql, arg, callback, handler);
	}

	public <ROW> List<ROW> select(String sql, Class<ROW> entityClass) throws SQLRuntimeException {
		return select(sql, null, entityClass);
	}

	public <ROW> List<ROW> select(String sql, Class<ROW> entityClass, IterationCallback<ROW> callback) throws SQLRuntimeException {
		return select(sql, null, entityClass, callback);
	}

	public List<Map<String, Object>> selectMap(String sql, Map<String, Object> arg) throws SQLRuntimeException {
		return selectMap(sql, arg, new ListIterationCallback<Map<String, Object>>());
	}

	public List<Map<String, Object>> selectMap(String sql, Map<String, Object> arg, IterationCallback<Map<String, Object>> callback)
			throws SQLRuntimeException {
		ResultSetHandler<?> handler = new MapResultSetHandler(callback);
		return select(sql, arg, callback, handler);
	}

	public List<Map<String, Object>> selectMap(String sql) throws SQLRuntimeException {
		return selectMap(sql, new ListIterationCallback<Map<String, Object>>());
	}

	public List<Map<String, Object>> selectMap(String sql, IterationCallback<Map<String, Object>> callback) throws SQLRuntimeException {
		return selectMap(sql, null, callback);
	}

	public SqlLogRegistry getSqlLogRegistry() {
		return sqlLogRegistry;
	}

	public static void setDefaultEncoding(String defaultEncoding) {
		DaoImpl.defaultEncoding = defaultEncoding;
	}

}
